package com.googlecode.perfectable.messageable;

import java.io.InputStream;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import com.google.common.base.Throwables;

public class XMLReceiver implements Receiver {
	private static final XMLInputFactory XML_INPUT_FACTORY = XMLInputFactory.newFactory();

	private final XMLEventReader reader;
	private final Unmarshaller unmarshaller;
	private final Endpoint transmissionSource;
	private final MessageProcessor<Message> processor;
	private final MessageQualifier qualifier;

	public XMLReceiver(InputStream inputStream, Endpoint transmissionSource,
			Set<Class<? extends Message>> knownReceivedClasses, MessageProcessor<Message> processor) {
		this.transmissionSource = transmissionSource;
		this.processor = processor;
		this.reader = createReader(inputStream);
		this.unmarshaller = createUnmarshaller(knownReceivedClasses);
		this.qualifier = MessageQualifier.create(knownReceivedClasses);
	}

	public void open() throws ReceptionException {
		try {
			Stream.initializeRead(this.reader);
		}
		catch(XMLStreamException e) {
			throw new ReceptionException(e);
		}
	}

	@Override
	public void cycle() throws ReceptionException, UnknownMessageException {
		StartElement startElement = nextStartElement();
		QName currentName = startElement.getName();
		Class<? extends Message> messageClass = this.qualifier.classForName(currentName);
		JAXBElement<? extends Message> jaxbElement;
		try {
			jaxbElement = this.unmarshaller.unmarshal(this.reader, messageClass);
		}
		catch(JAXBException e) {
			throw new ReceptionException(e);
		}
		Message message = jaxbElement.getValue();
		this.processor.process(message, this.transmissionSource);
	}

	private StartElement nextStartElement() throws StreamEndException, ReceptionException {
		while(true) {
			XMLEvent nextTag;
			try {
				nextTag = this.reader.peek();
			}
			catch(XMLStreamException e) {
				throw new ReceptionException(e);
			}
			if(nextTag == null) {
				throw new StreamEndException();
			}
			// MARK endElement stream
			if(nextTag.isStartElement()) {
				return nextTag.asStartElement();
			}
			this.reader.next();
		}
	}

	private static Unmarshaller createUnmarshaller(Set<Class<? extends Message>> knownClasses) {
		Class<?>[] knownClassesArray = knownClasses.toArray(new Class<?>[knownClasses.size()]);
		try {
			JAXBContext jaxbContext = JAXBContext.newInstance(knownClassesArray);
			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
			return unmarshaller;
		}
		catch(JAXBException e) {
			throw Throwables.propagate(e);
		}
	}

	private static XMLEventReader createReader(InputStream inputStream) {
		try {
			return XML_INPUT_FACTORY.createXMLEventReader(inputStream);
		}
		catch(XMLStreamException e) {
			throw Throwables.propagate(e);
		}
	}

}
